package com.virtual.repository.common.queries;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.StringUtils;

import com.virtual.repository.common.criteria.Criterion;

/**
 * A query depicting the options of selecting data from the database.
 * @author Zielony
 * @version 1.0
 */
public class Query {

	/**
	 * Full name of the entity stored in database 
	 */
	private String entityName;
	/**
	 * The <code>class</code> of the object(s) to be selected.
	 */
	private Class<?> selectedClass;
	
	/**
	 * A <code>List</code> of names of the fields to be selected. 
	 * <code>null</code> or <code>empty list</code> means all the possible fields are selected.
	 */
	private List<String> selectedFields = new ArrayList<>();
	
	/**
	 * The selection criteria. 
	 * <code>null</code> or <code>empty list</code> means the selected criteria are always true.
	 */
	private List<Criterion> selectionCriteria = new ArrayList<>();
	
	/**
	 * The limit on the number of selection results. 
	 * <code>null</code> means no sorting at all.
	 */
	private Integer limit;
	
	/**
	 * The <code>Map</code> relating the names of the fields according to which the results should be sorted with <code>
	 * true</code> meaning normal sort, while <code>false</code> means reverse-order sort. 
	 * <code>null</code> or <code>empty map</code> means no ordering of the results.
	 */
	private Map<String, Boolean> orderByMap = new HashMap<>();
	
	public Query() {
		
	}
	
	/**
	 * Creates a query
	 * @param selectedFields the fields to be selected.
	 * @param selectionCriteria the criteria of the selection.
	 * @param limit the limit on the number of results.
	 * @param orderByMap the <code>Map</code> of sorts on the results.
	 * @param entityName the name of the entity/table in the source database.
	 */
	public Query(Class<?> selectedClass, List<String> selectedFields, List<Criterion> selectionCriteria, 
			Integer limit, Map<String, Boolean> orderByMap, String entityName) {
		
		this(selectedClass, selectedFields, selectionCriteria, limit, orderByMap);
		this.entityName = entityName;
	}
	
	/**
	 * Creates a complete query.
	 * @param selectedClass the class of the selected entities.
	 * @param selectedFields the fields to be selected.
	 * @param selectionCriteria the criteria of selection.
	 * @param limit the limit on the number of results.
	 * @param orderByMap the <code>Map</code> of sorts on the results.
	 */
	public Query(Class<?> selectedClass, List<String> selectedFields, List<Criterion> selectionCriteria,
			Integer limit, Map<String, Boolean> orderByMap) {

		this.selectedClass = selectedClass;
		this.selectedFields = selectedFields;
		this.selectionCriteria = selectionCriteria;
		this.limit = limit ;
		this.orderByMap = orderByMap;
	}

	
	/*
	 * Gets: the selected class. 
	 * @return the selected class.
	 */
	public Class<?> getSelectedClass() {
		return selectedClass;
	}
	
	public void setSelectedClass(Class<?> selectedClass) {
		this.selectedClass = selectedClass;
	}
	
	/**
	 * Gets: the selected fields. 
	 * @return the selected fields.
	 */
	public List<String> getSelectedFields() {
		return this.selectedFields;
	}
	
	public void addSelectionField(String fieldName) {
		this.selectedFields.add(fieldName);
	}
	
	/**
	 * Gets: the selection criteria.
	 * @return the selection criteria.
	 */
	public List<Criterion> getSelectionCriteria() {
		return this.selectionCriteria;
	}
	
	public void addSelectionCriterion(Criterion criterion) {
		this.selectionCriteria.add(criterion);
	}
	
	public void setSelectionCriteria(List<Criterion> selectionCriteria) {
		this.selectionCriteria = selectionCriteria;
	}
	
	/**
	 * Gets: the limit on the number of results.
	 * @return the limit on the number of results.
	 */
	public Integer getLimit() {
		return limit;
	}

	public void setLimit(Integer limit) {
		this.limit = limit;
	}
	
	/**
	 * Gets: the sort rules.
	 * @return the sort rules.
	 */
	public Map<String, Boolean> getOrderByMap() {
		return orderByMap;
	}
	
	public void addOrderBy(String property, Boolean ascending) {
		this.orderByMap.put(property, ascending);
	}
	
	/**Gets: the full entity name stored in database
	 * @return the full entity name stored in database
	 */
	public String getEntityName() {
		return entityName;
	}

	public void setEntityName(String entityName) {
		this.entityName = entityName;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public String toString() {
		// Stringifying the Query to a para-SQL form to have a easily-readable query form.
		
		String result = "SELECT ";
		Iterator<?> i;
		
		/*
		 * Adding the selected fields if not null or selecting all fields. 
		 */
		if(selectedFields != null && !selectedFields.isEmpty()) {
			i = selectedFields.iterator();
			while(i.hasNext()) {
				result += i.next();
				if(i.hasNext()) {
					result += ",";
				}
				result += " ";
			}
		}
		else {
			result += "* ";
		}
		
		/*
		 * Adding the from and where clause. 
		 */
		String from = "";
		if (this.selectedClass != null) {
			from = this.selectedClass.getSimpleName();
		} else if (!StringUtils.isEmpty(this.entityName)) {
			from = this.entityName;
		}
		result += "FROM " + from + " WHERE ";
		
		/* 
		 * Adding the selection criteria or an always true criterion if null.
		 */
		if(selectionCriteria != null && !selectionCriteria.isEmpty()) {
			i = selectionCriteria.iterator();
			
			while(i.hasNext()) {
				result += i.next().toString();
				
				if(i.hasNext()) {
					result += ",";
				}
			}
		}
		else {
			result += "TRUE ";
		}
		
		/*
		 * Adding the sorting rules if they exist. 
		 */
		if(orderByMap != null && !orderByMap.isEmpty()) {
			i = orderByMap.keySet().iterator();
			
			result += " ORDER BY ";
			
			Object next;		
					
			while(i.hasNext()) {
				next =  i.next();
				
				result+= next;
				
				if(orderByMap.get(next)) {
					result += " ASC";
				}
				else {
					result += " DESC";
				}
				
				if(i.hasNext()) {
					result += ",";
				}
				
				result += " ";
			}
		}
		
		/*
		 * Adding the limit if it exists. 
		 */
		if(limit != null) {
			result += ("LIMIT " + limit.toString());
		}
		
		result += ";";
		
		return result;
	}
}